package sk.ownage.p1.featurestore;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.StreamCorruptedException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import android.graphics.Color;
import android.os.Environment;
import android.util.Log;

import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.Polyline;
import com.google.android.gms.maps.model.PolylineOptions;

public class TrackQuadTreeRoot {
	final String TAG = "TrackQuadTreeRoot";
	final LatLngBounds boundingBox = new LatLngBounds(
			new LatLng(-85.0, -180.0), new LatLng(85.0, 179.999999));
	static final int threshold = /* 1024 */1024;
	static final int DIRECTORY_MAX = TrackStore.DIRECTORY_MAX;;
	final static String rootPath = Environment.getExternalStorageDirectory()
			+ "/geometryStore" + "/tracks";
	public TrackQuadTreeNode northWest, northEast, southWest, southEast;
	public LatLng NW, SE, center;
	static int actualThreshold = threshold;
	int size = 0;
	boolean divided = false;
	boolean makeNew = false;
	boolean skip = false;
	File trackListFile;
	String path, exactpath;
	int actual_segment = 0;
	public TrackQuadTreeNode last_waypoint_node = null;
	double minLat = 85.0, maxLat = -85.0, minLng = 180.0, maxLng = -179.999999;
	LatLng last_waypoint = null, actual_waypoint = null, temp_waypoint = null;
	LatLng[] last_waypoints = new LatLng[5];
	LatLng[] actual_waypoints = new LatLng[5];
	TrackReader tr;
	String name = "root";
	Map<String, List<PolylineOptions>> old_List = new HashMap<String, List<PolylineOptions>>();
	Map<String, List<Polyline>> old_List2 = new HashMap<String, List<Polyline>>();
	Map<String, Long> pathsLastModified = new HashMap<String, Long>();
	Set<String> new_paths2 = new HashSet<String>();
	List<Polyline> old_polylines = new ArrayList<Polyline>();
	List<String> new_paths = new ArrayList<String>();
	Set<String> new_paths1 = new HashSet<String>();
	float last_zoom;
	double[] minDistances = { 80.0, 3.0, 0.24, 0.06, 0.00001 };
	double[] minDistances1 = { 80.0, 0.02695, 0.00216, 0.06, 0.00005};
	int zoomLevel = 0;

	public TrackQuadTreeRoot(int track_id, int _zoomLevel) {
		zoomLevel = _zoomLevel;

		NW = new LatLng(boundingBox.northeast.latitude,
				boundingBox.southwest.longitude);
		SE = new LatLng(boundingBox.southwest.latitude,
				boundingBox.northeast.longitude);
		center = getCenter(boundingBox);
		tr = new TrackReader(track_id);
		int folderId = track_id / DIRECTORY_MAX;
		int fileId = track_id % DIRECTORY_MAX;
		String folderPath = (((folderId / 100) > 0) ? "" : "0")
				+ (((folderId / 10) > 0) ? "" : "0") + folderId;
		String filePath = (((fileId / 100) > 0) ? "" : "0")
				+ (((fileId / 10) > 0) ? "" : "0") + fileId;
		path = rootPath + "/" + folderPath + "/" + filePath + "/" + zoomLevel;
		exactpath = path + "/" + "list.bin";
		size = loadSize();

		File geomSize = new File(path + "/size.bin");

		File folder = new File(path);
		if (!geomSize.exists()) {
			makeNew = true;
			actualThreshold = threshold;
		}

		// ak do tejto vetvy este mozeme pridavat body
		if (!skip) {
			if (!folder.exists()) {
				folder.mkdirs();
			}

			trackListFile = new File(exactpath);
			if (!trackListFile.exists())
				try {
					FileOutputStream fos = new FileOutputStream(trackListFile);
					ObjectOutputStream os = new ObjectOutputStream(
							new BufferedOutputStream(fos));
					os.close();
				} catch (FileNotFoundException e) {
					Log.e(TAG, e.toString());
					e.printStackTrace();
				} catch (IOException e) {
					Log.e(TAG, e.toString());
					e.printStackTrace();
				}
		}
	}
	
	public TrackQuadTreeRoot(int track_id,String _path, int _zoomLevel) {
		zoomLevel = _zoomLevel;
		NW = new LatLng(boundingBox.northeast.latitude,
				boundingBox.southwest.longitude);
		SE = new LatLng(boundingBox.southwest.latitude,
				boundingBox.northeast.longitude);
		center = getCenter(boundingBox);
		path = _path + zoomLevel;
		exactpath = path + "/" + "list.bin";
		size = loadSize();
		File geomSize = new File(path + "/size.bin");

		File folder = new File(path);
		if (!geomSize.exists()) {
			makeNew = true;
			actualThreshold = threshold;
		}
// ak do tejto vetvy este mozeme pridavat body
		if (!skip) {
			if (!folder.exists()) {
				folder.mkdirs();
			}

			trackListFile = new File(exactpath);
			if (!trackListFile.exists())
				try {
					FileOutputStream fos = new FileOutputStream(trackListFile);
					ObjectOutputStream os = new ObjectOutputStream(
							new BufferedOutputStream(fos));
					os.close();
				} catch (FileNotFoundException e) {
					Log.e(TAG, e.toString());
					e.printStackTrace();
				} catch (IOException e) {
					Log.e(TAG, e.toString());
					e.printStackTrace();
				}
		}
	}

	private LatLng getCenter(LatLngBounds bbox) {
		double averageLat = (bbox.northeast.latitude + bbox.southwest.latitude) / 2;
		double averageLon = (bbox.northeast.longitude + bbox.southwest.longitude) / 2;
		return new LatLng(averageLat, averageLon);
	}

	public void addWaypointToTrackGeometry(WaypointInfo waypointInfo) {
		this.addWaypointToTrackGeometry(waypointInfo.getPosition(),
				(waypointInfo.isNewSegment() ? 1 : 0));
	}

	public void addWaypointToTrackGeometry(LatLng wp, int new_segment) {

		if (!skip) {
			size = loadSize();
		}
		if (zoomLevel < 4)
			if (last_waypoint != null)
				if (getDistanceBetweenGeopoints(last_waypoint, wp) < minDistances[zoomLevel])
					return;

		ObjectInputStream is;
		if (size < actualThreshold && !divided) {
			if (minLat > wp.latitude)
				minLat = wp.latitude;
			if (maxLat < wp.latitude)
				maxLat = wp.latitude;
			if (minLng > wp.longitude)
				minLng = wp.longitude;
			if (maxLng < wp.longitude)
				maxLng = wp.longitude;
			try {
				FileOutputStream fos = new FileOutputStream(trackListFile, true);
				AppendingObjectOutputStream os = new AppendingObjectOutputStream(
						new BufferedOutputStream(fos));
				os.writeByte(new_segment);
				os.writeDouble(wp.latitude);
				os.writeDouble(wp.longitude);
				os.close();
			} catch (FileNotFoundException e) {
				Log.e(TAG, e.toString());
				e.printStackTrace();
			} catch (IOException e) {
				Log.e(TAG, e.toString());
				e.printStackTrace();
			}
			size++;
			saveSize();
			last_waypoint = actual_waypoint;
			actual_waypoint = wp;
		} else if (!divided) {
			LatLngBounds actualListBounds = new LatLngBounds(new LatLng(minLat,
					minLng), new LatLng(maxLat, maxLng));
			northEast = new TrackQuadTreeNode(this, name + ">NE",
					new LatLngBounds(center, boundingBox.northeast), path
							+ "/0", actualListBounds);
			northWest = new TrackQuadTreeNode(this, name + ">NW",
					new LatLngBounds(new LatLng(center.latitude,
							boundingBox.southwest.longitude), new LatLng(
							boundingBox.northeast.latitude, center.longitude)),
					path + "/1", actualListBounds);
			southEast = new TrackQuadTreeNode(this, name + ">SE",
					new LatLngBounds(new LatLng(boundingBox.southwest.latitude,
							center.longitude), new LatLng(center.latitude,
							boundingBox.northeast.longitude)), path + "/2",
					actualListBounds);
			southWest = new TrackQuadTreeNode(this, name + ">SW",
					new LatLngBounds(boundingBox.southwest, center), path
							+ "/3", actualListBounds);
			try {
				FileInputStream fis = new FileInputStream(trackListFile);
				is = new ObjectInputStream(new BufferedInputStream(fis));
				int count = size;
				actual_waypoint = null;
				while (count > 0) {
					int new_seg = is.readByte();
					double latitude1 = is.readDouble(), longitude1 = is
							.readDouble();
					LatLng waypoint = new LatLng(latitude1, longitude1);
					last_waypoint = actual_waypoint;
					temp_waypoint = waypoint;
					actual_waypoint = waypoint;
					divideQTree(last_waypoint, actual_waypoint, -1, null,
							new_seg);
					count--;
				}
				is.close();
				last_waypoint = actual_waypoint;
				temp_waypoint = wp;
				actual_waypoint = wp;
				divideQTree(last_waypoint, actual_waypoint, -1, null,
						new_segment);
				trackListFile.delete();
			} catch (StreamCorruptedException e) {
				Log.e(TAG, e.toString());
				e.printStackTrace();
			} catch (IOException e) {
				Log.e(TAG, e.toString());
				e.printStackTrace();
			}
			divided = true;
			skip = true;
			saveSize();
		} else {
			last_waypoint = actual_waypoint;
			temp_waypoint = wp;
			actual_waypoint = wp;
			divideQTree(last_waypoint, actual_waypoint, -1, null, new_segment);
		}
	}

	private void addWaypointToSubtree(LatLng wp, int new_segment) {
		if (wp.latitude >= center.latitude) {
			if (wp.longitude >= center.longitude) {
				northEast.addWaypoint(wp, new_segment);
			} else {
				northWest.addWaypoint(wp, new_segment);
			}
		} else {
			if (wp.longitude >= center.longitude) {
				southEast.addWaypoint(wp, new_segment);
			} else {
				southWest.addWaypoint(wp, new_segment);
			}
		}
	}

	public void divideQTree(LatLng actual_wp, LatLng new_wp, int from,
			TrackQuadTreeNode actual_qtnode, int new_segment) {
		TrackQuadTreeNode qtnode;
		if (actual_wp != null && new_segment != 1) {
			// ZISTIT TREENODE, KAM BOL PRIDANY PREDOSLY LATLNG BOD
			qtnode = (actual_qtnode != null) ? actual_qtnode
					: getLastUsedTreeNode(actual_wp);
			// hladam priesecniky s bounding boxom daneho nodu
			LatLng SW = new LatLng(qtnode.SE.latitude, qtnode.NW.longitude);
			LatLng NE = new LatLng(qtnode.NW.latitude, qtnode.SE.longitude);
			LatLng northSide, southSide, eastSide, westSide, new_actual_wp;
			// /////////////WEST SIDE/////////////////
			if (from != 1
					&& (westSide = getGeoPoint(qtnode.NW, SW, actual_wp, new_wp)) != null) {
				qtnode.addWaypoint(westSide, 0);

				qtnode = getLastUsedTreeNode(new LatLng(westSide.latitude,
						westSide.longitude - 0.000001));
				NE = new LatLng(qtnode.NW.latitude, qtnode.SE.longitude);
				new_actual_wp = getGeoPoint(qtnode.SE, NE, actual_wp, new_wp);
				qtnode.addWaypoint(new_actual_wp, 1);

				divideQTree(new_actual_wp, new_wp, 0, qtnode, 0);
			}
			// /////////////EAST SIDE/////////////////
			else if (from != 0
					&& (eastSide = getGeoPoint(qtnode.SE, NE, actual_wp, new_wp)) != null) {
				qtnode.addWaypoint(eastSide, 0);

				qtnode = getLastUsedTreeNode(new LatLng(eastSide.latitude,
						eastSide.longitude + 0.000001));
				SW = new LatLng(qtnode.SE.latitude, qtnode.NW.longitude);
				new_actual_wp = getGeoPoint(qtnode.NW, SW, actual_wp, new_wp);
				qtnode.addWaypoint(new_actual_wp, 1);

				divideQTree(new_actual_wp, new_wp, 1, qtnode, 0);
			}
			// /////////////NORTH SIDE/////////////////
			else if (from != 3
					&& (northSide = getGeoPoint(qtnode.NW, NE, actual_wp,
							new_wp)) != null) {
				qtnode.addWaypoint(northSide, 0);

				qtnode = getLastUsedTreeNode(new LatLng(
						northSide.latitude + 0.000001, northSide.longitude));
				SW = new LatLng(qtnode.SE.latitude, qtnode.NW.longitude);
				new_actual_wp = getGeoPoint(qtnode.SE, SW, actual_wp, new_wp);
				qtnode.addWaypoint(new_actual_wp, 1);

				divideQTree(new_actual_wp, new_wp, 2, qtnode, 0);
			}
			// /////////////SOUTH SIDE/////////////////
			else if (from != 2
					&& (southSide = getGeoPoint(qtnode.SE, SW, actual_wp,
							new_wp)) != null) {
				qtnode.addWaypoint(southSide, 0);

				qtnode = getLastUsedTreeNode(new LatLng(
						southSide.latitude - 0.000001, southSide.longitude));
				NE = new LatLng(qtnode.NW.latitude, qtnode.SE.longitude);
				new_actual_wp = getGeoPoint(qtnode.NW, NE, actual_wp, new_wp);
				qtnode.addWaypoint(new_actual_wp, 1);

				divideQTree(new_actual_wp, new_wp, 3, qtnode, 0);
			} else {
				qtnode.addWaypoint(new_wp, new_segment);
			}
		} else {
			addWaypointToSubtree(new_wp, new_segment);
		}
	}

	private double getDistanceBetweenGeopoints(LatLng from, LatLng to) {
		double lat1 = Math.toRadians(from.latitude), lon1 = Math
				.toRadians(from.longitude), lat2 = Math.toRadians(to.latitude), lon2 = Math
				.toRadians(to.longitude);
		double R = 6378.137; // km
		double result = (Math.acos(Math.sin(lat1) * Math.sin(lat2)
				+ Math.cos(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1)) * R);
		//Log.d(TAG, "distance is " + result);
		return result;
	}
	
	
	private TrackQuadTreeNode getLastUsedTreeNode(LatLng lastWP) {
		return getLastUsedTreeNode(
				lastWP,
				((lastWP.latitude >= center.latitude) ? ((lastWP.longitude >= center.longitude) ? northEast
						: northWest)
						: ((lastWP.longitude >= center.longitude) ? southEast
								: southWest)));

	}

	public TrackQuadTreeNode getLastUsedTreeNode(LatLng lastWP,
			TrackQuadTreeNode trackQTNode) {
		if (trackQTNode.northEast != null && trackQTNode.northWest != null
				&& trackQTNode.northEast != null
				&& trackQTNode.southWest != null) {
			return getLastUsedTreeNode(
					lastWP,
					((lastWP.latitude >= trackQTNode.center.latitude) ? ((lastWP.longitude >= trackQTNode.center.longitude) ? trackQTNode.northEast
							: trackQTNode.northWest)
							: ((lastWP.longitude >= trackQTNode.center.longitude) ? trackQTNode.southEast
									: trackQTNode.southWest)));
		} else {
			return trackQTNode;
		}
	}

	public void cleanMap() {
		for (Polyline old_polyline : old_polylines) {
			old_polyline.remove();
		}
		old_polylines = new ArrayList<Polyline>();
		for (List<Polyline> polylineList : old_List2.values()) {
			if (polylineList != null)
				for (Polyline polyline : polylineList) {
					polyline.remove();
				}
		}
		old_List2 = new HashMap<String, List<Polyline>>();
	}

	public void getPolylines(GoogleMap gmap) {
		for (Polyline old_polyline : old_polylines) {
			old_polyline.remove();
		}
		old_polylines = new ArrayList<Polyline>();
		new_paths2 = new HashSet<String>();
		Map<String, List<Polyline>> result = getPolylines(gmap.getProjection()
				.getVisibleRegion().latLngBounds, "", boundingBox.southwest,
				boundingBox.northeast, gmap);
		List<String> to_remove = new ArrayList<String>();
		for (String path : old_List2.keySet()) {
			if (!new_paths2.contains(path)) {
				to_remove.add(path);
			}
		}
		for (String path : to_remove) {
			pathsLastModified.remove(path);
			for (Polyline polyline : old_List2.get(path))
				polyline.remove();
			old_List2.remove(path);
		}
		old_List2.putAll(result);
	}

	public Map<String, List<Polyline>> getPolylines(LatLngBounds boundingBox1,
			String _path, LatLng _SW, LatLng _NE, GoogleMap gmap) {

		double averageLat = (_SW.latitude + _NE.latitude) / 2;
		double averageLon = (_SW.longitude + _NE.longitude) / 2;
		LatLng tempCenter = new LatLng(averageLat, averageLon);

		Map<String, List<Polyline>> result = new HashMap<String, List<Polyline>>();

		LatLngBounds bbox2 = new LatLngBounds(_SW, _NE);
		boolean condition = (boundingBox1.contains(new LatLng(_NE.latitude,
				_SW.longitude))
				|| boundingBox1
						.contains(new LatLng(_SW.latitude, _NE.longitude))
				|| boundingBox1.contains(_NE)
				|| boundingBox1.contains(_SW)
				|| boundingBox1.contains(tempCenter)
				|| bbox2.contains(boundingBox1.northeast)
				|| bbox2.contains(boundingBox1.southwest)
				|| bbox2.contains(new LatLng(boundingBox1.southwest.latitude,
						boundingBox1.northeast.longitude)) || bbox2
				.contains(new LatLng(boundingBox1.northeast.latitude,
						boundingBox1.southwest.longitude)));
		if (!condition)
			return result;
		FileInputStream fis;
		ObjectInputStream is;
		if (_path.equals(""))
			_path = path;
		File sizePath = new File(_path + "/" + "size.bin");
		int flag = 0;
		boolean divided = false;
		if (sizePath.exists())
			try {
				fis = new FileInputStream(sizePath);
				is = new ObjectInputStream(new BufferedInputStream(fis));
				flag = is.readInt();
				divided = is.readBoolean();
				is.close();
			} catch (Exception e) {
				Log.d(TAG, e.toString());
			}
		File inputStream = new File(_path + "/" + "list.bin");
		if (divided) {
			File tempFile;
			tempFile = new File(_path + "/" + 0 + "/size.bin");
			if (tempFile.exists())
				result.putAll(getPolylines(boundingBox1, _path + "/" + 0,
						tempCenter, _NE, gmap));
			tempFile = new File(_path + "/" + 1 + "/size.bin");
			if (tempFile.exists())
				result.putAll(getPolylines(boundingBox1, _path + "/" + 1,
						new LatLng(tempCenter.latitude, _SW.longitude),
						new LatLng(_NE.latitude, tempCenter.longitude), gmap));
			tempFile = new File(_path + "/" + 2 + "/size.bin");
			if (tempFile.exists())
				result.putAll(getPolylines(boundingBox1, _path + "/" + 2,
						new LatLng(_SW.latitude, tempCenter.longitude),
						new LatLng(tempCenter.latitude, _NE.longitude), gmap));
			tempFile = new File(_path + "/" + 3 + "/size.bin");
			if (tempFile.exists())
				result.putAll(getPolylines(boundingBox1, _path + "/" + 3, _SW,
						tempCenter, gmap));

		} else {
			if (old_List2.containsKey(_path)
					&& (pathsLastModified.get(_path) != null && inputStream
							.lastModified() <= pathsLastModified.get(_path)))
				new_paths2.add(_path);
			else
				try {
					pathsLastModified.put(_path, inputStream.lastModified());
					fis = new FileInputStream(inputStream);
					is = new ObjectInputStream(new BufferedInputStream(fis));
					PolylineOptions tempPolylineOptions = new PolylineOptions();
					while (flag > 0) {
						int copy = is.readByte();
						if (copy == 1) {
							if (tempPolylineOptions != null) {
								if (!result.containsKey(_path))
									result.put(_path, new ArrayList<Polyline>());

								new_paths2.add(_path);
								result.get(_path)
										.add(gmap
												.addPolyline(tempPolylineOptions.color(Color.rgb(
														(int) Math
																.round(Math
																		.random() * 255),
														(int) Math.round(Math
																.random() * 255),
														(int) Math.round(Math
																.random() * 255)))));
							}
							tempPolylineOptions = new PolylineOptions();
						}
						tempPolylineOptions.add(new LatLng(is.readDouble(), is
								.readDouble()));
						flag--;
					}
					if (!result.containsKey(_path))
						result.put(_path, new ArrayList<Polyline>());
					new_paths2.add(_path);
					if (!old_List.containsKey(_path))
						result.get(_path)
								.add(gmap.addPolyline(tempPolylineOptions.color(Color.rgb(
										(int) Math.round(Math.random() * 255),
										(int) Math.round(Math.random() * 255),
										(int) Math.round(Math.random() * 255)))));
				} catch (EOFException e) {
					Log.e(TAG, e.toString());
					e.printStackTrace();
				} catch (FileNotFoundException e) {
					Log.e(TAG, e.toString());
					e.printStackTrace();
				} catch (StreamCorruptedException e) {
					Log.e(TAG, e.toString());
					e.printStackTrace();
				} catch (IOException e) {
					Log.e(TAG, e.toString());
					e.printStackTrace();
				}
		}
		return result;
	}

	// ci sa cislo nachadza v danom intervale
	private boolean InInterval(double what, int from, int to) {
		return (what >= from) && (what <= to);
	}

	// najde prisecnik(GeoPoint ktory pretina hranice daneho stvorca
	// QuadTreeNodu a cesty, ktora prechadza z jedneho stvorca do ineho)
	private LatLng getGeoPoint(LatLng a, LatLng b, LatLng c, LatLng d) {
		double x1 = a.longitude, x2 = b.longitude, x3 = c.longitude, x4 = d.longitude, y1 = a.latitude, y2 = b.latitude, y3 = c.latitude, y4 = d.latitude;
		double ua, ub, ca, cb;
		double menovatel = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
		if (menovatel == 0) {
			return null;
		} else {
			ca = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3));
			cb = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3));
			ua = ca / menovatel;
			ub = cb / menovatel;
			if (InInterval(ua, 0, 1) && InInterval(ub, 0, 1)) {
				return new LatLng((y1 + ua * (y2 - y1)), (x1 + ua * (x2 - x1)));
			} else {
				return null;
			}
		}
	}

	private int loadSize() {
		File sizePath = new File(path + "/" + "size.bin");
		int result = 0;
		if (sizePath.exists())
			try {
				FileInputStream fis = new FileInputStream(sizePath);
				ObjectInputStream is = new ObjectInputStream(
						new BufferedInputStream(fis));
				result = is.readInt();
				divided = is.readBoolean();
				skip = is.readBoolean();
				actualThreshold = is.readInt();
				is.close();
			} catch (Exception e) {
				Log.d(TAG, e.toString());
			}
		return result;
	}

	private void saveSize() {
		File sizePath = new File(path + "/" + "size.bin");
		try {
			FileOutputStream fos = new FileOutputStream(sizePath);
			ObjectOutputStream os = new ObjectOutputStream(
					new BufferedOutputStream(fos));
			os.writeInt(size);
			os.writeBoolean(divided);
			os.writeBoolean(skip);
			os.writeInt(threshold);
			os.close();
		} catch (Exception e) {
			Log.d(TAG, e.toString());
		}
	}

}
